﻿/*	
	stencil - action script 3 isometric game engine.

	http://code.google.com/p/stencilas3/
	
	Copyright (c) 2010 Max Zhuravkov, All Rights Reserved.

	Permission is hereby granted, free of charge, to any person obtaining a copy of
	this software and associated documentation files (the "Software"), to deal in
	the Software without restriction, including without limitation the rights to
	use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
	of the Software, and to permit persons to whom the Software is furnished to do
	so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/
package stencil.core.engine.display {
	
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.Dictionary;
	import flash.events.MouseEvent;
	import flash.utils.getTimer;
	import stencil.core.engine.ISceneObject;
	import stencil.core.errors.ArgumentIsNullError;
	import stencil.core.engine.IGameObject;
	import stencil.core.engine.Position;
	import stencil.core.engine.IGrid;
	import stencil.core.engine.Bounds;
	import stencil.core.engine.display.DisplayState;
	import stencil.core.engine.display.IDisplayObject;
	import stencil.core.engine.display.IDepthSorting;
	import stencil.core.engine.events.DisplayListEvent;
	import stencil.core.engine.utils.getClassName;
	
	/**
	 * Provides basic operations for managing display list content.
	 * 
	 * Render phase is splitted into six (or five when depth sort is omitted) stages:
	 * 
	 * 1) pre-render stage (preRender method).
	 * 2) calulcation of display objects positions and adjustment of their visibility according to current view port. 
	 * if depth sorting should be performed (4a then 4b) otherwise 4b.
	 * 4a) depth sorting of visible objects.
	 * 4b) for each visible object its render method is called.
	 * 5) post-render stage (postRender method).
	 * 
	 * Tile/non-tile object support:
	 * 
	 * Creates different display object containers for its tile and non-tile children  
	 * so that they can not be sorted together. NOTICE: Depth sorting is only applied to 
	 * non-tile children of this display list so TILE OBJECTS ARE NOT SORTED AT ALL.
	 * 
	 * @author Max Zhuravkov
	 */
	public class SimpleIsometricDisplayList extends AbstractDisplayList {
		
		private var _children:Array = [];
		
		private var _tiles:Array = [];
		
		private var _childrenHash:Dictionary = new Dictionary();
		
		private var _visibleObjects:Array = [];
		
		private var _objectContainer:DisplayObjectContainer = new Sprite();
		
		private var _tileContainer:DisplayObjectContainer = new Sprite();
		
		private var _tilesToFront:Boolean = false;
		
		private var flagTilesToFrontChanged:Boolean = false;
		
		private var flagUpdateVisibility:Boolean = false;
		
		private var flagInvalidate:Boolean = false;
		
		//------------------------------------------------------------------------------------------------
		// Constructor
		//------------------------------------------------------------------------------------------------
		
		public function SimpleIsometricDisplayList(id:String = null, calculateTilePositions:Boolean = false) {
			super(id, calculateTilePositions);
			
			this.container.addChild(this._tileContainer);
			this.container.addChild(this._objectContainer);
		}
		
		//------------------------------------------------------------------------------------------------
		// Public properties
		//------------------------------------------------------------------------------------------------
		
		/**
		 * Indicates whether tiles should be placed above non-tile objects or not. 
		 */
		public function set tilesToFront(value:Boolean):void {
			if (this._tilesToFront == value) {
				return;
			}
			this._tilesToFront = value;
			this.flagTilesToFrontChanged = true;
		}
		
		public function get tilesToFront():Boolean {
			return this._tilesToFront;
		}
		
		/**
		 * @inheritDoc
		 */
		override public function get children():Array { 
			return this._children; 
		}
		
		/**
		 * @inheritDoc
		 */
		override public function get tiles():Array { 
			return this._tiles; 
		}
		
		//------------------------------------------------------------------------------------------------
		// Public methods
		//------------------------------------------------------------------------------------------------
		
		/**
		 * @inheritDoc
		 */
		override public function add(child:IDisplayObject):void {
			if (this.contains(child)) {
				return;
			}
			
			var sprite:DisplayObject = child.content;
			var sceneObject:ISceneObject = child.owner;
			var position:Position = sceneObject.position;
			var bounds:Bounds = sceneObject.bounds;
			
			if (!position) {
				throw new Error("position of " + sceneObject.id + " should be not null.")
			}
			
			if (!bounds) {
				throw new Error("bounds of " + sceneObject.id + " should be not null.")
			}
			
			sprite.visible = sceneObject.flagTile;
			this._childrenHash[child.id] = child;
			
			if (sceneObject.flagTile) {
				this._tileContainer.addChild(sprite);
				
				if (this.calculateTilePositions) {
					
					var p:Point = this.grid.getCellPositionAt(position, true);
					
					child.content.x = p.x;
					child.content.y = p.y;
					
				}
				
				this._tiles.push(child);
			}else {
				
				this._objectContainer.addChild(sprite);
				
				var state:DisplayState = this.createState(child);
				child.displayState = state;
				
				this._children.push(child);
			}
			
			this.flagUpdateVisibility = true;
			this.sort();
			
			this.childAdded(child);
			this.dispatchEvent(new DisplayListEvent(DisplayListEvent.STN_CHILD_ADDED, child));
		}
		
		/**
		 * @inheritDoc
		 */
		override public function remove(child:IDisplayObject):void {
			if (!this.contains(child)) {
				throw new ArgumentError("The given display object [" + child.id +"] has not been found within display list[" + this.id + "]");
			}
			
			var sprite:DisplayObject = child.content;
			var sceneObject:ISceneObject = child.owner;
			
			if (sceneObject.flagTile) {
				
				this._tileContainer.removeChild(sprite);
				
				var tIndex:int = this._tiles.indexOf(child);
				if(tIndex >= 0){
					this._tiles.splice(tIndex, 1);
				}
				
			}else {
				var cIndex:int = this._children.indexOf(child);
				var vIndex:int = this._visibleObjects.indexOf(child);
				
				if (cIndex >= 0) {
					child.displayState = null;
					this._children.splice(cIndex, 1);
					
					if(sprite.parent){
						this._objectContainer.removeChild(sprite);
					}
				}
				
				if (vIndex >= 0) {
					this._visibleObjects.splice(vIndex, 1);
				}
				
			}
			
			this.childRemoved(child);
			this.flagUpdateVisibility = true;
			this.sort();
			
			this._childrenHash[child.id] = null;
			delete this._childrenHash[child.id];
			
			this.dispatchEvent(new DisplayListEvent(DisplayListEvent.STN_CHILD_REMOVED, child));
			
		}
		
		/**
		 * @inheritDoc
		 */
		override public function contains(child:IDisplayObject):Boolean {
			this.checkChild(child);
			
			if (child.owner.flagTile) {
				return this._tiles.indexOf(child) >= 0 ? true : false;
			}else {
				return this._childrenHash[child.id] ? true : false;
			}
		}
		
		/**
		 * @inheritDoc
		 */
		override public function find(id:String):IDisplayObject {
			if (id == null) {
				throw new ArgumentIsNullError("id");
			}
			
			for each(var tile:IDisplayObject in this._tiles) {
				if (tile.id == id) {
					return tile;
				}
			}
		
			return this._childrenHash[id];
		}
		
		/**
		 * @inheritDoc
		 */
		override public function update(child:IDisplayObject):void {
			if (!this.contains(child)) {
				throw new ArgumentError("The given display object has not been found within display list[" + this.id + "]");
			}
			
			var index:int = this._children.indexOf(child);
			
			if (index >= 0) {
				var state:DisplayState = child.displayState;
				
				if (!child.owner.position.equals(state.position)) {
					state.shouldBeUpdated = true;
					
					this.flagUpdateVisibility = true;
					this.sort();
				}
			}
		}
		
		/**
		 * @inheritDoc
		 */
		override public function invalidate():void {
			this.flagSort = true;
			this.flagInvalidate = true;
			this.flagUpdateVisibility = true;
		}
		
		//------------------------------------------------------------------------------------------------
		// Protected methods
		//------------------------------------------------------------------------------------------------
		
		protected function checkChild(child:IDisplayObject):void {
			if (!child) {
				throw new ArgumentIsNullError("child");
			}
			if (!child.id) {
				throw new ArgumentError("Child indentifier should be not null [" + child + "]");
			}
			
			if (!child.owner) {
				throw new ArgumentError("'owner' property has not been set. [" + child.id + "] of class '" + getClassName(child) + "'.");
			}
			
			if (!child.owner.bounds) {
				throw new ArgumentError("Bounds has not been set. [" + child.id + "] of class '" + getClassName(child) + "'.");
			}
			
			if (!child.owner.position) {
				throw new ArgumentError("Child position has not been set. [" + child.id + "] of class '" + getClassName(child) + "'.");
			}
			
			if (!child.owner.flagTile && !child.owner.dimensions) {
				throw new ArgumentError("Dimensions has not been set. [" + child.id + "] of class '" + getClassName(child) + "'.");
			}
		}
		
		protected function createState(object:IDisplayObject):DisplayState {
			var state:DisplayState = new DisplayState();
			state.parent = this;
			state.position = object.owner.position.clone();
			state.shouldBeUpdated = true;
			return state;
		}
		
		override protected function preRender(viewPort:Rectangle):void {
			if (this.flagTilesToFrontChanged) {
				
				if(this.tilesToFront){
					this.container.setChildIndex(this._tileContainer, 1);
				}else {
					this.container.setChildIndex(this._objectContainer, 1);
				}
				
				this.flagTilesToFrontChanged = false;
			}
		}
		
		override protected function postRender(viewPort:Rectangle):void {
			this.flagUpdateVisibility = false;
			this.flagInvalidate = false;
		}
		
		protected function childAdded(child:IDisplayObject):void {
			
		}
		
		protected function childRemoved(child:IDisplayObject):void {
		
		}
		
		/**
		 * Updates positions of display list children and adjusts their visiblity if necessary.
		 */
		override protected function updateDisplayObjects(viewPort:Rectangle, viewPortChanged:Boolean):Array {
			var t:Number = getTimer();
			
			for each(var child:IDisplayObject in this.children) {
				var state:DisplayState = child.displayState;
				
				if (this.flagInvalidate || state.shouldBeUpdated ) {
					var visibilityChanged:Boolean = viewPortChanged;
				
					if (state.shouldBeUpdated || this.flagInvalidate) {
						var position:Position = child.owner.position;
						
						state.shouldBeUpdated = false;
						state.position.x = position.x;
						state.position.y = position.y;
						
						this.updatePosition(child, position);
						this.depthSortingAlgorithm.calculateIndex(child);
						
						visibilityChanged = true;
					}
					
					if (visibilityChanged || viewPortChanged) {
						this.updateVisibility(child, viewPort, this._visibleObjects);
					}
				}
			}
			
			t = getTimer() - t;
			if (t > 1) {
				trace("update took " + t)
			}
			
			return this._visibleObjects;
		}
		
		protected function updatePosition(child:IDisplayObject, position:Position):void {
			var sprite:DisplayObject = child.content;
			var pos:Point = this.grid.getCellPositionAt(position, true);
			var p:Point = pos.clone();
			
			pos.x = Math.round(pos.x);
			pos.y = Math.round(pos.y);
			
			sprite.x = pos.x;
			sprite.y = pos.y;
		}
		
		protected function updateVisibility(child:IDisplayObject, viewPort:Rectangle, visibleObjects:Array):void {
			var rect:Rectangle = this.displayObjectRect(child);
			var index:int = visibleObjects.indexOf(child);
			var visible:Boolean = viewPort.intersects(rect);
			
			child.content.visible = visible;
			
			if (visible) {
				if (index == -1) {
					visibleObjects.push(child);
				}
			}else {
				if (index != -1) {
					visibleObjects.splice(index, 1);
				}
			}
		}
		
		/**
		 * 
		 */
		override protected function sortAndRenderDisplayObjects(visibleObjects:Array, viewPort:Rectangle, time:Number):void {
			var t:Number = getTimer()
			
			this.depthSortingAlgorithm.sort(visibleObjects);
			
			var length:int = visibleObjects.length;
			
			for (var i:int = 0; i < length; i ++ ) {
				var object:IDisplayObject = visibleObjects[i];
				object.displayState.depth = i;
				
				this.addToDisplayList(object, i);
				object.render(viewPort, time);
			}
			
			t = (getTimer()- t);
			
			if(t > 0){
				trace("sort (" + length + ") time is " + t  + " ms.");
			}
			
		}
		
		protected function addToDisplayList(child:IDisplayObject, index:int):void {
			var container:DisplayObjectContainer = child.owner.flagTile ? this._tileContainer : this._objectContainer;
			var sprite:DisplayObject = child.content;
			if (child.owner.flagTile || !sprite.parent) {
				if (index < 0) {
					container.addChild(sprite);
				}else {
					container.addChildAt(sprite, index);
				}
			}else {
				container.setChildIndex(sprite, index);
			}
		}
		
		protected function displayObjectRect(child:IDisplayObject):Rectangle {
			var sprite:DisplayObject = child.content;
			var bounds:Bounds = child.owner.bounds;
			var bx:Number = sprite.x + this.grid.globalOffset.x;
			var by:Number = sprite.y + this.grid.globalOffset.y;
			
			bounds.translate(bx, by);

			return bounds.rect;
		}
	}
	
}